﻿/****************************************************************************
Copyright (c) 2013-2015 scutgame.com

http://www.scutgame.com

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
****************************************************************************/
using System;
using System.Linq;
using System.Collections.Generic;
using System.Net;
using System.Text;
using System.Web;
using Newtonsoft.Json;
using System.Threading;
using System.Collections.Concurrent;
using System.Threading.Tasks;
using MongoDB.Bson;
using ChessServer.Log;
using ChessServer.Common;
using ChessServer.Security;

namespace ChessServer.Net
{
    /// <summary>
    /// 
    /// </summary>
    public class SessionPushEventArgs : EventArgs
    {
        /// <summary>
        /// 
        /// </summary>
        public ExSocket Socket { get; set; }
        /// <summary>
        /// 
        /// </summary>
        public int OpCode { get; set; }
        /// <summary>
        /// 
        /// </summary>
        public byte[] Data { get; set; }
        /// <summary>
        /// 
        /// </summary>
        public int Offset { get; set; }
        /// <summary>
        /// 
        /// </summary>
        public int Count { get; set; }
    }
    /// <summary>
    /// 用户会话
    /// </summary>
    public class GameSession
    {
        private static ConcurrentDictionary<Guid, GameSession> _globalSession;
        private static ConcurrentDictionary<Int64, Guid> _userHash;
        private static ConcurrentDictionary<string, Guid> _remoteHash;
        private static SyncTimer clearTime;
        private static string sessionRedisKey = "__GLOBAL_SESSIONS";
        private static HashSet<GameSession> changeSession;

        static GameSession()
        {
            HeartbeatTimeout = 60;//60s
            RequestTimeout = 500;
            Timeout = 2 * 60 * 60;//2H
            clearTime = new SyncTimer(OnClearSession, 6000, 60000);
            clearTime.Start();
            changeSession = new HashSet<GameSession>();
            _globalSession = new ConcurrentDictionary<Guid, GameSession>();
            _userHash = new ConcurrentDictionary<Int64, Guid>();
            _remoteHash = new ConcurrentDictionary<string, Guid>();
            OurDebug.Log("Start LoadUnLineData");
        }

        private static void OnChangedSave(GameSession session)
        {
            try
            {
                if (session == null || !session.IsAuthorized)
                    return;
            }
            catch (Exception er)
            {
                OurDebug.LogErrorFormat("Save session to redis faild,{0}", er);
            }
        }

        /// <summary>
        /// 更新从当前连接记录中的超时标记
        /// </summary>
        private static void SaveTo()
        {
            try
            {
                var tempSet = Interlocked.Exchange(ref changeSession, new HashSet<GameSession>());
                if (tempSet.Count == 0) return;

                var keys = new List<string>();
                var values = new List<string>();
                foreach (GameSession session in tempSet)
                {
                    if (session.LastActivityTime.AddMinutes(1) > DateTime.Now || session.User == null) continue;
                    //var user = MathUtils.ToJson(session.User);
                    var user = session.UserId + "";
                    keys.Add(string.Format("{0}:{1}", sessionRedisKey, session.KeyCode.ToString("N")));
                    values.Add(user);
                }
            }
            catch (Exception er)
            {
                OurDebug.LogErrorFormat("Save session to redis faild,{0}", er);
            }
        }

        /// <summary>
        /// Count
        /// </summary>
        public static int Count { get { return _globalSession.Count; } }

        /// <summary>
        /// Heartbeat timeout(sec), default 60s
        /// </summary>
        public static int HeartbeatTimeout { get; set; }

        /// <summary>
        /// session timeout(sec), default 2h
        /// </summary>
        public static int Timeout { get; set; }
        /// <summary>
        /// Request timeout(ms), default 1s
        /// </summary>
        public static int RequestTimeout { get; set; }


        private static string GenerateSid(Guid guid)
        {
            return string.Format("s_{0}|{1}|{2}", guid.ToString("N"), 1, ConfigUtils.ProductServerId);
        }

        /// <summary>
        /// 
        /// </summary>
        public static void ClearSession(Predicate<GameSession> match)
        {
            foreach (var pair in _globalSession)
            {
                var session = pair.Value;
                if (session == null) continue;
                if (match(session))
                {
                    session.Reset();
                }
            }
        }

        private static void OnClearSession(object state)
        {
            try
            {
                foreach (var pair in _globalSession)
                {
                    var session = pair.Value;
                    if (session == null) continue;

                    if (session.CheckExpired())
                    {
                        OurDebug.LogFormat("User {0} sessionId{1} is expire {2}({3}sec)",
                            session.UserId,
                            session.SessionId,
                            session.LastActivityTime,
                            Timeout);
                        session.DoHeartbeatTimeout();
                        session.Reset();

                    }
                    else if (!session.IsHeartbeatTimeout &&
                        HeartbeatTimeout > 0 &&
                        session.LastActivityTime < MathUtils.Now.AddSeconds(-HeartbeatTimeout))
                    {
                        session.DoHeartbeatTimeout();
                    }
                }
                SaveTo();
            }
            catch (Exception er)
            {
                OurDebug.LogErrorFormat("ClearSession error:{0}", er);
            }
        }

        /// <summary>
        /// Add session to cache
        /// </summary>
        /// <param name="keyCode"></param>
        /// <param name="socket"></param>
        /// <param name="appServer"></param>
        public static GameSession CreateNew(ExSocket socket, TcpSocketServer appServer) {
            GameSession session;
            session = new GameSession ( socket, appServer );
            _globalSession [session.KeyCode] = session;
            return session;
        }

        /// <summary>
        /// Recover session
        /// </summary>
        /// <param name="session"></param>
        /// <param name="newSessionKey"></param>
        /// <param name="socket"></param>
        /// <param name="appServer"></param>
        /// <returns></returns>
        public static void Recover(GameSession session, Guid newSessionKey, ExSocket socket, TcpSocketServer appServer)
        {
            var newSession = Get(newSessionKey);
            if (session != null &&
                newSession != null &&
                session != newSession)
            {
                try
                {
                    session._exSocket.Close();
                }
                catch
                {
                }
                //modify socket's keycod not found reason
                socket.Reset(session.KeyCode);
                session._exSocket = socket;
                session.AppServer = appServer;
                GameSession temp;
                if (_globalSession.TryRemove(newSessionKey, out temp))
                {
                    OnChangedSave(session);
                }
            }
        }

        /// <summary>
        /// Get session by userid
        /// </summary>
        /// <param name="userId"></param>
        /// <returns></returns>
        public static GameSession Get(Int64 userId)
        {
            Guid val;
            return _userHash.TryGetValue(userId, out val) ? Get(val) : null;
        }

        internal static Guid GetUserBindSid(Int64 userId)
        {
            Guid val;
            if (_userHash.TryGetValue(userId, out val))
            {
                return val;
            }
            return Guid.Empty;
        }
        /// <summary>
        /// Get session by sessionid.
        /// </summary>
        /// <param name="sessionId"></param>
        /// <returns></returns>
        public static GameSession Get(string sessionId)
        {
            GameSession session = null;
            Guid hashCode;
            if (TryParseSessionId(sessionId, out hashCode))
            {
                session = Get(hashCode);
            }
            return session;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="sessionId"></param>
        /// <param name="sid"></param>
        /// <returns></returns>
        public static bool TryParseSessionId(string sessionId, out Guid sid)
        {
            sid = Guid.Empty;
            string[] arr = (sessionId ?? "").Split('_', '|');
            if (arr.Length > 1)
            {
                Guid hashCode;
                if (Guid.TryParse(arr[1], out hashCode))
                {
                    sid = hashCode;
                    return true;
                }
            }
            return false;
        }

        /// <summary>
        /// Get session by ExSocket.
        /// </summary>
        /// <param name="key"></param>
        /// <returns></returns>
        public static GameSession Get(Guid key)
        {
            GameSession session;
            return _globalSession.TryGetValue(key, out session) ? session : null;
        }

        /// <summary>
        /// Get all session
        /// </summary>
        /// <returns></returns>
        public static List<GameSession> GetAll()
        {
            return _globalSession.Values.ToList();
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="proxyId"></param>
        /// <returns></returns>
        public static GameSession GetRemote(string proxyId)
        {
            Guid val;
            return _remoteHash.TryGetValue(proxyId, out val) ? Get(val) : null;
        }
        /// <summary>
        /// Get remote all
        /// </summary>
        /// <returns></returns>
        public static List<GameSession> GetRemoteAll()
        {
            return _remoteHash.Select(pair => Get(pair.Value)).ToList();
        }

        /// <summary>
        /// 
        /// </summary>
        /// <returns></returns>
        public static List<GameSession> GetOnlineAll()
        {
            return GetOnlineAll(HeartbeatTimeout);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <returns></returns>
        public static List<GameSession> GetOnlineAll(int delayTime)
        {
            List<GameSession> list = new List<GameSession>();
            foreach (var pair in _globalSession)
            {
                var session = pair.Value;
                if (!session.IsRemote &&
                    (!session.IsSocket || session.Connected) &&
                    session.LastActivityTime > MathUtils.Now.AddSeconds(-delayTime))
                {
                    list.Add(session);
                }
            }
            return list;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="request"></param>
        /// <returns></returns>
        public static GameSession GetSessionByCookie(HttpRequest request)
        {
            var cookie = request.Cookies.Get("sid");
            if (cookie != null && !string.IsNullOrEmpty(cookie.Value))
            {
                return Get(cookie.Value);
            }
            return null;
        }
        /// <summary>
        /// 
        /// </summary>
        /// <param name="request"></param>
        /// <returns></returns>
        public static GameSession GetSessionByCookie(HttpListenerRequest request)
        {
            var cookie = request.Cookies["sid"];
            if (cookie != null && !string.IsNullOrEmpty(cookie.Value))
            {
                return Get(cookie.Value);
            }
            return null;
        }

        /// <summary>
        /// huhu 给连接发送某个消息。
        /// </summary>
        /// <param name="sessionList"></param>
        /// <param name="msg">Encoded IExtensible</param>
        /// <param name="msgid">PBConf.conf().GetMsgId("m_user_g2c")</param>
        public static void BroadcastMsg(List<GameSession> sessionList, byte[] msg, int msgid)
        {
            var ms = new MessageStructure(msgid, msg);

            foreach (var session in sessionList)
            {
                session.SendAsync(OpCode.Text, ms.Data, 0
                , ms.Data.Length, delegate (SocketAsyncResult asyncResult)
                {
                    if (asyncResult.Result != ResultCode.Success)
                        OurDebug.Log("The results of data send:{0} fail", msgid);
                });
            }
        }

        /// <summary>
        /// 给单个对象发消息。
        /// </summary>
        /// <param name="session"></param>
        /// <param name="msg">Encoded IExtensible</param>
        /// <param name="msgid">PBConf.conf().GetMsgId("m_user_g2c")</param>
        public static void SendMsg(GameSession session, byte[] msg, int msgid)
        {
            if (session == null || !session.IsSocket)
            {
                OurDebug.LogError(string.Format("消息发送失败，没有连接或者是连接断开的 msgid：{0} session:{1}", msgid, session));
                return;
            }
            var ms = new MessageStructure( msgid , msg );
            session.SendAsync(OpCode.Text, ms.Data, 0, ms.Data.Length
            , delegate (SocketAsyncResult asyncResult)
            {
                if (asyncResult.Result != ResultCode.Success)
                    OurDebug.Log("The results of data send:{0} fail", msgid);
            });
        }

        /// <summary>
        /// 给单个对象发消息。
        /// </summary>
        /// <param name="session"></param>
        /// <param name="msg">Encoded IExtensible</param>
        /// <param name="msgid">PBConf.conf().GetMsgId("m_user_g2c")</param>
        /// <param name="millisecond">延迟毫秒</param>
        public static async System.Threading.Tasks.Task SendMsgDelay(GameSession session, byte[] msg, int msgid, int millisecond = 10)
        {
            await Task.Delay(millisecond);
            if (session == null || !session.IsSocket)
            {
                OurDebug.LogError(string.Format("消息发送失败，没有连接或者是连接断开的 msgid：{0} session:{1}", msgid, session));
                return;
            }
            var ms = new MessageStructure( msgid, msg );
            session.SendAsync(OpCode.Text, ms.Data, 0, ms.Data.Length, delegate (SocketAsyncResult asyncResult) {
                if (asyncResult.Result != ResultCode.Success)
                    OurDebug.Log("The results of data send:{0} fail", msgid);
            });
        }

        private string _remoteAddress;
        private int _isInSession;
        private ExSocket _exSocket;
        /// <summary>
        /// 
        /// </summary>
        public TcpSocketServer AppServer { get; private set; }

        /// <summary>
        /// Heartbeat Timeout event
        /// </summary>
        public event Action<GameSession> HeartbeatTimeoutHandle;

        private void DoHeartbeatTimeout()
        {
            try
            {
                IsHeartbeatTimeout = true;
                Action<GameSession> handler = HeartbeatTimeoutHandle;
                if (handler != null) handler(this);
            }
            catch (Exception)
            {
            }
        }

        private GameSession(ExSocket exSocket, TcpSocketServer appServer){
            KeyCode = exSocket.HashCode;
            SessionId = GenerateSid ( KeyCode );
            InitSocket (exSocket, appServer);
        }

        internal void InitSocket(ExSocket exSocket, TcpSocketServer appServer)
        {
            _exSocket = exSocket;
            if (_exSocket != null) _remoteAddress = _exSocket.RemoteEndPoint.ToString();
            AppServer = appServer;
            if (User != null)
            {
                //update userid with sid.
                _userHash[UserId] = KeyCode;
            }
        }

        /// <summary>
        /// 心跳包刷新连接状态
        /// </summary>
        public void Refresh()
        {
            //changeSession.Add(this);
            IsTimeout = false;
            IsHeartbeatTimeout = false;
            LastActivityTime = DateTime.Now;
            if (User != null)
            {
                User.RefleshOnlineDate();
            }
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="user"></param>
        public void Bind(IUser user)
        {
            if (user == null) return;
            var userId = user.GetUserId();
            if (userId > 0)
            {
                //解除UserId与前一次的Session连接对象绑定
                Guid sid;
                if (_userHash.TryGetValue(userId, out sid) && sid != KeyCode)
                {
                    var session = Get(sid);
                    if (session != null)
                    {
                        //防止先后问题
                        if (session.LastActivityTime < this.LastActivityTime || session.LastActivityTime > DateTime.Now/*超过服务器本地时间的都是测试过程中的，给过了。*/)
                        {
                            session.UnBind();
                            //Expire(session, 0);
                        }
                        else
                        {
                            return;
                        }
                    }
                }
            }
            _userHash[userId] = KeyCode;
            User = user;
            OnChangedSave(this);
        }

        /// <summary>
        /// 
        /// </summary>
        public void UnBind()
        {
            User = null;
            OldSessionId = SessionId;
        }

        /// <summary>
        /// Is authorized.
        /// </summary>
        [JsonIgnore]
        public bool IsAuthorized
        {
            get { return User != null && User.GetUserId() > 0; }
        }

        /// <summary>
        /// sdk登陆验证结果 非0表示未验证通过
        /// 初始为可以登录，当走了sdk之后发现有错就不处理它的消息了
        /// </summary>
        [JsonIgnore]
        public int SdkLoginCheckCode = 0;

        /// <summary>
        /// sdk登陆验证结果 非0表示未验证通过
        /// 初始为可以登录，当走了sdk之后发现有错就不处理它的消息了
        /// </summary>
        [JsonIgnore]
        public string SdkLoginCheckMessage = "";

        /// <summary>
        /// Is proxy server session
        /// </summary>
        //[JsonIgnore]
        //public bool IsProxyServer
        //{
        //    get { return ProxySid != Guid.Empty && UserId == 0; }
        //}

        /// <summary>
        /// 
        /// </summary>
        [JsonIgnore]
        public bool IsRemote
        {
            get { return !string.IsNullOrEmpty(ProxyId); }
        }
        /// <summary>
        /// Close
        /// </summary>
        public void Close()
        {
            GameSession session;
            if (_globalSession.TryGetValue(KeyCode, out session) && session._exSocket != null)
            {
                //设置Socket为Closed的状态, 并未将物理连接马上中断
                session._exSocket.IsClosed = true;
            }
        }

        /// <summary>
        /// 
        /// </summary>
        /// <returns>true:is expired</returns>
        private bool CheckExpired()
        {
            return LastActivityTime < MathUtils.Now.AddSeconds(-Timeout);
        }

        private void Reset()
        {
            IsTimeout = true;
            if (_exSocket != null)
            {
                try
                {
                    //设置Socket为Closed的状态, 并未将物理连接马上中断
                    _exSocket.IsClosed = true;
                    _exSocket.Close();
                }
                catch { }
            }
            Guid code;
            if (_userHash.TryRemove(UserId, out code))
            {
                UnBind();
            }

            if (!string.IsNullOrEmpty(ProxyId)) _remoteHash.TryRemove(ProxyId, out code);
            GameSession session;
            _globalSession.TryRemove ( KeyCode, out session );
        }


        /// <summary>
        /// Remote end address
        /// </summary>
        [JsonIgnore]
        public string RemoteAddress
        {
            get { return _remoteAddress; }
            internal set { _remoteAddress = value; }
        }
        /// <summary>
        /// Remote end address
        /// </summary>
        [JsonIgnore]
        [Obsolete]
        public string EndAddress
        {
            get { return _remoteAddress; }
        }

        /// <summary>
        /// Old sessionid
        /// </summary>
        [JsonIgnore]
        public string OldSessionId { get; set; }

        /// <summary>
        /// key code
        /// </summary>
        public Guid KeyCode { get; private set; }

        /// <summary>
        /// SessionId
        /// </summary>
        public string SessionId { get; private set; }

        /// <summary>
        /// login UserId
        /// </summary>
        [JsonIgnore]
        public Int64 UserId { get { return User != null ? User.GetUserId() : 0; } }

        /// <summary>
        /// User
        /// </summary>
        public IUser User { get; private set; }

        /// <summary>
        /// 远程代理客户端的会话ID
        /// </summary>
        //[ProtoMember(4)]
        //public Guid ProxySid { get; internal set; }

        /// <summary>
        /// 最后活动时间
        /// </summary>
        public DateTime LastActivityTime { get; internal set; }

        /// <summary>
        /// 是否会话超时
        /// </summary>
        [JsonIgnore]
        public bool IsTimeout { get; set; }

        /// <summary>
        /// 
        /// </summary>
        [JsonIgnore]
        public bool IsHeartbeatTimeout { get; set; }

        [JsonIgnore]
        private string _proxyId;

        /// <summary>
        /// 远程代理客户端的标识ID
        /// </summary>
        public string ProxyId
        {
            get { return _proxyId; }
            set
            {
                _proxyId = value;
                if (!string.IsNullOrEmpty(_proxyId))
                {
                    _remoteHash[_proxyId] = KeyCode;
                }
            }
        }

        /// <summary>
        /// 是否标识关闭状态
        /// </summary>
        [JsonIgnore]
        public bool IsClosed
        {
            get { return _exSocket != null && _exSocket.IsClosed; }
        }

        /// <summary>
        /// 是否已连接
        /// </summary>
        [JsonIgnore]
        public bool Connected
        {
            get
            {
                try
                {
                    return _exSocket != null && _exSocket.Connected;
                }
                catch
                {
                    return false;
                }
            }
        }

        [JsonIgnore]
        public int msgthreadid = -1;

        /// <summary>
        /// is socket
        /// </summary>
        [JsonIgnore]
        public bool IsSocket
        {
            get { return _exSocket != null; }
        }

        /// <summary>
        /// Post send to client
        /// </summary>
        /// <param name="opCode"></param>
        /// <param name="data"></param>
        /// <param name="offset"></param>
        /// <param name="count"></param>
        /// <param name="callback"></param>
        private bool PostSend(sbyte opCode, byte[] data, int offset, int count, Action<SocketAsyncResult> callback, string debuguserid = "")
        {
            if (!IsSocket)
            {
                OurDebug.LogErrorFormat("Session does not support the push message");
                return false;
            }
            if (data == null || data.Length == 0)
            {
                return false;
            }
            AppServer.PostSend(_exSocket, opCode, data, offset, count, callback, debuguserid);
            return true;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="data"></param>
        /// <param name="offset"></param>
        /// <param name="count"></param>
        /// <returns></returns>
        //public async System.Threading.Tasks.Task<bool> SendAsync(byte[] data, int offset, int count)
        //{
        //    if (!IsRemote)
        //    {
        //        data = CheckAdditionalHead(data, ProxySid);
        //    }
        //    return await SendAsync(OpCode.Binary, data, offset, count, result => { });
        //}

        /// <summary>
        /// Send async, add 16 len head
        /// </summary>
        /// <param name="opCode"></param>
        /// <param name="data"></param>
        /// <param name="offset"></param>
        /// <param name="count"></param>
        /// <param name="callback"></param>
        /// <returns></returns>
        public bool SendAsync(sbyte opCode, byte[] data, int offset, int count, Action<SocketAsyncResult> callback, string debuguserid = "")
        {
            return PostSend(opCode, data, 0, data.Length, callback, debuguserid);
        }
        /// <summary>
        /// 检查加头16位ssid
        /// </summary>
        /// <param name="data"></param>
        /// <param name="ssid"></param>
        /// <returns></returns>
        //private static byte[] CheckAdditionalHead(byte[] data, Guid ssid)
        //{
        //    if (ssid == Guid.Empty)
        //    {
        //        return data;
        //    }
        //    var buffer = new byte[data.Length + 16];
        //    Buffer.BlockCopy(ssid.ToByteArray(), 0, buffer, 0, 16);
        //    Buffer.BlockCopy(data, 0, buffer, 16, data.Length);
        //    return buffer;
        //}

        internal bool EnterSession()
        {
            return Interlocked.CompareExchange(ref _isInSession, 1, 0) == 0;
        }

        internal void ExitSession()
        {
            Interlocked.Exchange(ref _isInSession, 0);
        }

    }
}